package com.youngjun.common.security.service.impl;

import com.youngjun.common.security.constant.PKIContat;
import com.youngjun.common.security.exception.CertificationAuthenticationException;
import com.youngjun.common.security.service.PkiService;
import com.youngjun.common.security.service.UserDetailsService;
import com.youngjun.common.util.SpringUtils;
import com.youngjun.common.util.PKIUtils;
import com.youngjun.common.util.PropUtils;
import com.youngjun.common.util.StringUtils;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.DERBMPString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.asn1.x509.CRLReason;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.cert.X509CRLHolder;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v2CRLBuilder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CRLConverter;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.jce.interfaces.PKCS12BagAttributeCarrier;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.*;
import java.security.cert.*;
import java.security.spec.InvalidKeySpecException;
import java.util.*;

public abstract class AbstractPkiService implements PkiService {

    private final static Logger logger = LoggerFactory.getLogger(AbstractPkiService.class);

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    protected KeyPairGenerator keyPairGenerator;
    protected X509Certificate rootCertificate;
    protected X509Certificate agentCertificate;
    protected PrivateKey agentPrivateKey;
    protected X509CRL x509CRL;
    protected X509v2CRLBuilder x509v2CRLBuilder;

    @PostConstruct
    public void init() throws NoSuchProviderException, NoSuchAlgorithmException, IOException, CertificateException, InvalidKeySpecException, OperatorCreationException, CRLException {
        this.keyPairGenerator = KeyPairGenerator.getInstance(PKIContat.DEFAULT_ALGORITHM, PKIContat.DEFAULT_PROVIDER);
        this.keyPairGenerator.initialize(2048, new SecureRandom());

        String rootCertificateBase64Str = PropUtils.get("pki.certs.root.cert");
        String agentCertificateBase64Str = PropUtils.get("pki.certs.agent.cert");
        String agentKeyBase64Str = PropUtils.get("pki.certs.agent.key");

        if (rootCertificateBase64Str == null || agentCertificateBase64Str == null || agentKeyBase64Str == null) {
            throw new RuntimeException("必须设置根证书和私钥信息: pki.certs.root.cert pki.certs.agent.cert pki.certs.agent.key in application.yml");
        }

        ByteArrayInputStream rootCertificateInputStream = new ByteArrayInputStream(rootCertificateBase64Str.getBytes(Charset.forName("UTF-8")));
        this.rootCertificate = PKIUtils.loadCertificateFromPEM(PKIContat.DEFAULT_CERTIFICATE_TYPE, rootCertificateInputStream);

        ByteArrayInputStream agentCertificateInputStream = new ByteArrayInputStream(agentCertificateBase64Str.getBytes(Charset.forName("UTF-8")));
        this.agentCertificate = PKIUtils.loadCertificateFromPEM(PKIContat.DEFAULT_CERTIFICATE_TYPE, agentCertificateInputStream);

        this.agentPrivateKey = PKIUtils.loadPrivateKeyFromPEM(agentKeyBase64Str, PKIContat.DEFAULT_ALGORITHM);
        //吊销证书列表
        List<X509Certificate> revokedCertificates = new ArrayList<>();
        List<String> revokedCertificateList = getRevokedCertificateList();
        for(String revokedCertificate : revokedCertificateList) {
            if(StringUtils.isNotEmpty(revokedCertificate)) {
                ByteArrayInputStream revokedCertificateInputStream = new ByteArrayInputStream(revokedCertificate.getBytes(Charset.forName("UTF-8")));
                X509Certificate x509Certificate = null;
                try {
                    x509Certificate = PKIUtils.loadCertificateFromPEM(PKIContat.DEFAULT_CERTIFICATE_TYPE, revokedCertificateInputStream);
                    revokedCertificates.add(x509Certificate) ;
                } catch (CertificateException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        X509v2CRLBuilder builder = new X509v2CRLBuilder(
                new X500Name(getAgentCertificate().getSubjectDN().getName())
                , new Date());

        for (X509Certificate certificate : revokedCertificates) {
            builder.addCRLEntry(certificate.getSerialNumber(), new Date(), CRLReason.privilegeWithdrawn);
        }

        this.x509v2CRLBuilder = builder;
        X509CRLHolder crlHolder = builder.build(
                new JcaContentSignerBuilder(PKIContat.DEFAULT_SIGNATURE_ALGORITHM)
                        .setProvider(PKIContat.DEFAULT_PROVIDER)
                        .build(getRootPrivateKey()));

        JcaX509CRLConverter converter = new JcaX509CRLConverter()
                .setProvider(PKIContat.DEFAULT_PROVIDER);
        this.x509CRL = converter.getCRL(crlHolder);
    }

    @Override
    public KeyPair generateKeyPair() {
        return this.keyPairGenerator.generateKeyPair();
    }

    @Override
    public X509Certificate getRootCertificate() {
        return this.rootCertificate;
    }

    @Override
    public X509Certificate getAgentCertificate() {
        return this.agentCertificate;
    }

    @Override
    public PrivateKey getRootPrivateKey() {
        return this.agentPrivateKey;
    }

    @Override
    public X509CRL getX509CRL() {
        return this.x509CRL;
    }

    @Override
    public PKCS10CertificationRequest createCSR(X500Name x500Name, KeyPair keyPair) throws OperatorCreationException {
        PKCS10CertificationRequestBuilder builder = new JcaPKCS10CertificationRequestBuilder(x500Name, keyPair.getPublic());
        JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(PKIContat.DEFAULT_SIGNATURE_ALGORITHM);
        ContentSigner signer = signerBuilder.build(keyPair.getPrivate());
        return builder.build(signer);
    }

    @Override
    public void verfyCert(X509Certificate certificate) {
        try {
            if (getAgentCertificate() == null) {
                throw new CertificationAuthenticationException("您的证书异常，请重新通过平台Web页面获取。(error code 1001)");
            }
            try {
                certificate.checkValidity(new Date());
            } catch (CertificateExpiredException | CertificateNotYetValidException e) {
                throw new CertificationAuthenticationException("您的证书已过期，请重新通过平台Web页面获取。(error code 1002)");
            }
            try {
                certificate.verify(getAgentCertificate().getPublicKey());
            } catch (CertificateException | NoSuchAlgorithmException |
                    InvalidKeyException | NoSuchProviderException |
                    SignatureException e) {
                throw new CertificationAuthenticationException("您的证书异常，请重新通过平台Web页面获取。(error code 1003)");
            }

            if (getX509CRL().isRevoked(certificate)) {
                System.out.println("");
                System.out.println("===" + certificate.getSubjectDN().getName());
                throw new CertificationAuthenticationException("您的证书已被吊销，请重新通过平台Web页面获取。(error code 1004)");
            } else {
                String agentName = certificate.getSubjectDN().getName();
                String name = null;
                String username = null;
                if (StringUtils.isNotEmpty(agentName)) {
                    String[] strFormt1 = agentName.split(",");// [1].split("=")
                    for (String fSplit : strFormt1) {
                        if (fSplit == null || fSplit.length() == 0) {
                            continue;
                        }
                        String[] sSplit = fSplit.split("=");
                        if (sSplit == null || sSplit.length < 2) {
                            continue;
                        }
                        String sSplitFirst = sSplit[0].replaceAll(" ", "");
                        if ("O".equals(sSplitFirst)) {
                            name = sSplit[1];
                        } else if ("CN".equals(sSplitFirst)) {
                            username = sSplit[1];
                        }
                    }

                    if (name == null || username == null) {
                        throw new CertificationAuthenticationException("您的证书异常，请重新通过平台Web页面获取。(error code 1005)");
                    }
                    String certificateCode = SpringUtils.getBean(UserDetailsService.class).loadCertificateByUsername(username);
                    if (StringUtils.isEmpty(certificateCode)) {
                        throw new CertificationAuthenticationException("您的证书异常，请重新通过平台Web页面获取。(error code 1007)");
                    }
                    String certificatePEM = PKIUtils.exportToPEM(certificate);
                    if (StringUtils.isEmpty(certificatePEM)) {
                        throw new CertificationAuthenticationException("您的证书异常，请重新通过平台Web页面获取。(error code 1007)");
                    }
                    if (!certificatePEM.replaceAll("\r\n|\r|\n", "").equals(certificateCode.replaceAll("\r\n|\r|\n", ""))) {
                        throw new CertificationAuthenticationException("您的证书异常，请重新通过平台Web页面获取。(error code 1008)");
                    }
                    //succeed
                } else {
                    throw new CertificationAuthenticationException("您的证书异常，请重新通过平台Web页面获取。(error code 1009)");
                }
            }
        } catch (CertificationAuthenticationException e) {
            throw e;
        } catch (Exception e) {
            logger.warn(e.getMessage());
            e.printStackTrace();
        }
    }

    @Override
    public X509Certificate signCert(PKCS10CertificationRequest csr) throws IOException, NoSuchAlgorithmException, OperatorCreationException, CertificateException, InvalidKeyException, NoSuchProviderException, SignatureException {
        X509Certificate rootCertificate = getAgentCertificate();
        X500Name issuer = new X500Name(rootCertificate.getSubjectX500Principal().getName());
        String cn = IETFUtils.valueToString(csr.getSubject().getRDNs(BCStyle.CN)[0].getFirst().getValue());

        BigInteger serial = new BigInteger(32, new SecureRandom());

        Calendar start = Calendar.getInstance();
        Calendar expiry = Calendar.getInstance();
        expiry.add(Calendar.YEAR, PKIContat.DEFAULT_EXPIRED_YEARS);

        X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(
                issuer,
                serial,
                start.getTime(),
                expiry.getTime(),
                csr.getSubject(),
                csr.getSubjectPublicKeyInfo());


        JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();

        certificateBuilder.addExtension(
                Extension.subjectKeyIdentifier,
                false,
                extUtils.createSubjectKeyIdentifier(csr.getSubjectPublicKeyInfo()));

        certificateBuilder.addExtension(
                Extension.authorityKeyIdentifier,
                false,
                extUtils.createAuthorityKeyIdentifier(rootCertificate.getPublicKey()));

        certificateBuilder.addExtension(
                Extension.basicConstraints,
                false,
                new BasicConstraints(false));

        certificateBuilder.addExtension(
                Extension.keyUsage,
                false,
                new KeyUsage(KeyUsage.digitalSignature));

        ASN1EncodableVector vector = new ASN1EncodableVector();
        vector.add(KeyPurposeId.id_kp_clientAuth);

        certificateBuilder.addExtension(
                Extension.extendedKeyUsage,
                false,
                new DERSequence(vector));

        PrivateKey rootPrivateKey = getRootPrivateKey();

        ContentSigner signer = new JcaContentSignerBuilder(PKIContat.DEFAULT_SIGNATURE_ALGORITHM)
                .setProvider(new BouncyCastleProvider()).build(rootPrivateKey);

        X509CertificateHolder holder = certificateBuilder.build(signer);

        X509Certificate certificate = new JcaX509CertificateConverter()
                .setProvider(new BouncyCastleProvider()).getCertificate(holder);

        certificate.checkValidity(new Date());

        certificate.verify(rootCertificate.getPublicKey());

        PKCS12BagAttributeCarrier bagAttr = (PKCS12BagAttributeCarrier) certificate;

        // this is actually optional - but if you want to have control
        // over setting the friendly name this is the way to domain it...
        bagAttr.setBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName, new DERBMPString(cn));

        return certificate;
    }

    @Override
    public void addX509CRL(String certificate) {
        if (StringUtils.isNotEmpty(certificate)) {
            ByteArrayInputStream revokedCertificateInputStream = new ByteArrayInputStream(certificate.getBytes(Charset.forName("UTF-8")));
            X509Certificate x509Certificate = null;
            try {
                x509Certificate = PKIUtils.loadCertificateFromPEM(PKIContat.DEFAULT_CERTIFICATE_TYPE, revokedCertificateInputStream);
                this.x509v2CRLBuilder.addCRLEntry(x509Certificate.getSerialNumber(), new Date(), CRLReason.privilegeWithdrawn);
                X509CRLHolder crlHolder = this.x509v2CRLBuilder.build(
                        new JcaContentSignerBuilder(PKIContat.DEFAULT_SIGNATURE_ALGORITHM)
                                .setProvider(PKIContat.DEFAULT_PROVIDER)
                                .build(getRootPrivateKey()));

                JcaX509CRLConverter converter = new JcaX509CRLConverter()
                        .setProvider(PKIContat.DEFAULT_PROVIDER);
                this.x509CRL = converter.getCRL(crlHolder);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    abstract List<String> getRevokedCertificateList();

}
